/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.startup;

import org.apache.catalina.*;
import org.apache.catalina.security.SecurityConfig;
import org.apache.juli.ClassLoaderLogManager;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.digester.Digester;
import org.apache.tomcat.util.digester.Rule;
import org.apache.tomcat.util.digester.RuleSet;
import org.apache.tomcat.util.log.SystemLogHandler;
import org.apache.tomcat.util.res.StringManager;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;

import java.io.*;
import java.lang.reflect.Constructor;
import java.net.ConnectException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.LogManager;

/**
 * Startup/Shutdown shell program for Catalina.  The following command line
 * options are recognized:
 * <ul>
 * <li><b>-config {pathname}</b> - Set the pathname of the configuration file
 * to be processed.  If a relative path is specified, it will be
 * interpreted as relative to the directory pathname specified by the
 * "catalina.base" system property.   [conf/server.xml]
 * <li><b>-help</b>      - Display usage information.
 * <li><b>-nonaming</b>  - Disable naming support.
 * <li><b>configtest</b> - Try to test the config
 * <li><b>start</b>      - Start an instance of Catalina.
 * <li><b>stop</b>       - Stop the currently running instance of Catalina.
 * </u>
 * <p>
 * Should do the same thing as Embedded, but using a server.xml file.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class Catalina {

	/**
	 * The string manager for this package.
	 */
	protected static final StringManager sm =
			StringManager.getManager(Constants.Package);


	// ----------------------------------------------------- Instance Variables
	private static final org.apache.juli.logging.Log log =
			org.apache.juli.logging.LogFactory.getLog(Catalina.class);
	/**
	 * Use await.
	 */
	protected boolean await = false;

	// XXX Should be moved to embedded
	/**
	 * Pathname to the server configuration file.
	 */
	protected String configFile = "conf/server.xml";
	/**
	 * The shared extensions class loader for this server.
	 */
	protected ClassLoader parentClassLoader =
			Catalina.class.getClassLoader();
	/**
	 * The server component we are starting or stopping.
	 */
	protected Server server = null;
	/**
	 * Are we starting a new server?
	 *
	 * @deprecated Unused - will be removed in Tomcat 8.0.x
	 */
	@Deprecated
	protected boolean starting = false;
	/**
	 * Are we stopping an existing server?
	 *
	 * @deprecated Unused - will be removed in Tomcat 8.0.x
	 */
	@Deprecated
	protected boolean stopping = false;
	/**
	 * Use shutdown hook flag.
	 */
	protected boolean useShutdownHook = true;
	/**
	 * Shutdown hook.
	 */
	protected Thread shutdownHook = null;


	// ----------------------------------------------------------- Constructors
	/**
	 * Is naming enabled ?
	 */
	protected boolean useNaming = true;


	// ------------------------------------------------------------- Properties

	public Catalina() {
		setSecurityProtection();
		ExceptionUtils.preload();
	}

	/**
	 * @deprecated Use {@link #setConfigFile(String)}
	 */
	@Deprecated
	public void setConfig(String file) {
		configFile = file;
	}

	public String getConfigFile() {
		return configFile;
	}

	public void setConfigFile(String file) {
		configFile = file;
	}

	public boolean getUseShutdownHook() {
		return useShutdownHook;
	}

	public void setUseShutdownHook(boolean useShutdownHook) {
		this.useShutdownHook = useShutdownHook;
	}

	public ClassLoader getParentClassLoader() {
		if (parentClassLoader != null) {
			return (parentClassLoader);
		}
		return ClassLoader.getSystemClassLoader();
	}

	/**
	 * Set the shared extensions class loader.
	 *
	 * @param parentClassLoader The shared extensions class loader.
	 */
	public void setParentClassLoader(ClassLoader parentClassLoader) {
		this.parentClassLoader = parentClassLoader;
	}

	public Server getServer() {
		return server;
	}

	public void setServer(Server server) {
		this.server = server;
	}

	/**
	 * Return true if naming is enabled.
	 */
	public boolean isUseNaming() {
		return (this.useNaming);
	}

	/**
	 * Enables or disables naming support.
	 *
	 * @param useNaming The new use naming value
	 */
	public void setUseNaming(boolean useNaming) {
		this.useNaming = useNaming;
	}

	public boolean isAwait() {
		return await;
	}

	// ------------------------------------------------------ Protected Methods

	public void setAwait(boolean b) {
		await = b;
	}

	/**
	 * Process the specified command line arguments, and return
	 * <code>true</code> if we should continue processing; otherwise
	 * return <code>false</code>.
	 *
	 * @param args Command line arguments to process
	 */
	protected boolean arguments(String args[]) {

		boolean isConfig = false;

		if (args.length < 1) {
			usage();
			return (false);
		}

		for (int i = 0; i < args.length; i++) {
			if (isConfig) {
				configFile = args[i];
				isConfig = false;
			} else if (args[i].equals("-config")) {
				isConfig = true;
			} else if (args[i].equals("-nonaming")) {
				setUseNaming(false);
			} else if (args[i].equals("-help")) {
				usage();
				return (false);
			} else if (args[i].equals("start")) {
				starting = true;
				stopping = false;
			} else if (args[i].equals("configtest")) {
				starting = true;
				stopping = false;
			} else if (args[i].equals("stop")) {
				starting = false;
				stopping = true;
			} else {
				usage();
				return (false);
			}
		}

		return (true);

	}

	/**
	 * Return a File object representing our configuration file.
	 */
	protected File configFile() {

		File file = new File(configFile);
		if (!file.isAbsolute()) {
			file = new File(System.getProperty(Globals.CATALINA_BASE_PROP), configFile);
		}
		return (file);

	}

	/**
	 * Create and configure the Digester we will be using for startup.
	 */
	protected Digester createStartDigester() {
		long t1 = System.currentTimeMillis();
		// Initialize the digester
		Digester digester = new Digester();
		digester.setValidating(false);
		digester.setRulesValidation(true);
		HashMap<Class<?>, List<String>> fakeAttributes =
				new HashMap<Class<?>, List<String>>();
		ArrayList<String> attrs = new ArrayList<String>();
		attrs.add("className");
		fakeAttributes.put(Object.class, attrs);
		digester.setFakeAttributes(fakeAttributes);
		digester.setUseContextClassLoader(true);

		// Configure the actions we will be using
		digester.addObjectCreate("Server",
				"org.apache.catalina.core.StandardServer",
				"className");
		digester.addSetProperties("Server");
		digester.addSetNext("Server",
				"setServer",
				"org.apache.catalina.Server");

		digester.addObjectCreate("Server/GlobalNamingResources",
				"org.apache.catalina.deploy.NamingResources");
		digester.addSetProperties("Server/GlobalNamingResources");
		digester.addSetNext("Server/GlobalNamingResources",
				"setGlobalNamingResources",
				"org.apache.catalina.deploy.NamingResources");

		digester.addObjectCreate("Server/Listener",
				null, // MUST be specified in the element
				"className");
		digester.addSetProperties("Server/Listener");
		digester.addSetNext("Server/Listener",
				"addLifecycleListener",
				"org.apache.catalina.LifecycleListener");

		digester.addObjectCreate("Server/Service",
				"org.apache.catalina.core.StandardService",
				"className");
		digester.addSetProperties("Server/Service");
		digester.addSetNext("Server/Service",
				"addService",
				"org.apache.catalina.Service");

		digester.addObjectCreate("Server/Service/Listener",
				null, // MUST be specified in the element
				"className");
		digester.addSetProperties("Server/Service/Listener");
		digester.addSetNext("Server/Service/Listener",
				"addLifecycleListener",
				"org.apache.catalina.LifecycleListener");

		//Executor
		digester.addObjectCreate("Server/Service/Executor",
				"org.apache.catalina.core.StandardThreadExecutor",
				"className");
		digester.addSetProperties("Server/Service/Executor");

		digester.addSetNext("Server/Service/Executor",
				"addExecutor",
				"org.apache.catalina.Executor");


		digester.addRule("Server/Service/Connector",
				new ConnectorCreateRule());
		digester.addRule("Server/Service/Connector",
				new SetAllPropertiesRule(new String[]{"executor"}));
		digester.addSetNext("Server/Service/Connector",
				"addConnector",
				"org.apache.catalina.connector.Connector");


		digester.addObjectCreate("Server/Service/Connector/Listener",
				null, // MUST be specified in the element
				"className");
		digester.addSetProperties("Server/Service/Connector/Listener");
		digester.addSetNext("Server/Service/Connector/Listener",
				"addLifecycleListener",
				"org.apache.catalina.LifecycleListener");

		// Add RuleSets for nested elements
		digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
		digester.addRuleSet(new EngineRuleSet("Server/Service/"));
		digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
		digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
		addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");
		digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));

		// When the 'engine' is found, set the parentClassLoader.
		digester.addRule("Server/Service/Engine",
				new SetParentClassLoaderRule(parentClassLoader));
		addClusterRuleSet(digester, "Server/Service/Engine/Cluster/");

		long t2 = System.currentTimeMillis();
		if (log.isDebugEnabled()) {
			log.debug("Digester for server.xml created " + (t2 - t1));
		}
		return (digester);

	}

	/**
	 * Cluster support is optional. The JARs may have been removed.
	 */
	private void addClusterRuleSet(Digester digester, String prefix) {
		Class<?> clazz = null;
		Constructor<?> constructor = null;
		try {
			clazz = Class.forName("org.apache.catalina.ha.ClusterRuleSet");
			constructor = clazz.getConstructor(String.class);
			RuleSet ruleSet = (RuleSet) constructor.newInstance(prefix);
			digester.addRuleSet(ruleSet);
		} catch (Exception e) {
			if (log.isDebugEnabled()) {
				log.debug(sm.getString("catalina.noCluster",
						e.getClass().getName() + ": " + e.getMessage()), e);
			} else if (log.isInfoEnabled()) {
				log.info(sm.getString("catalina.noCluster",
						e.getClass().getName() + ": " + e.getMessage()));
			}
		}
	}

	/**
	 * Create and configure the Digester we will be using for shutdown.
	 */
	protected Digester createStopDigester() {

		// Initialize the digester
		Digester digester = new Digester();
		digester.setUseContextClassLoader(true);

		// Configure the rules we need for shutting down
		digester.addObjectCreate("Server",
				"org.apache.catalina.core.StandardServer",
				"className");
		digester.addSetProperties("Server");
		digester.addSetNext("Server",
				"setServer",
				"org.apache.catalina.Server");

		return (digester);

	}

	public void stopServer() {
		stopServer(null);
	}

	public void stopServer(String[] arguments) {

		if (arguments != null) {
			arguments(arguments);
		}

		Server s = getServer();
		if (s == null) {
			// Create and execute our Digester
			Digester digester = createStopDigester();
			File file = configFile();
			FileInputStream fis = null;
			try {
				InputSource is =
						new InputSource(file.toURI().toURL().toString());
				fis = new FileInputStream(file);
				is.setByteStream(fis);
				digester.push(this);
				digester.parse(is);
			} catch (Exception e) {
				log.error("Catalina.stop: ", e);
				System.exit(1);
			} finally {
				if (fis != null) {
					try {
						fis.close();
					} catch (IOException e) {
						// Ignore
					}
				}
			}
		} else {
			// Server object already present. Must be running as a service
			try {
				s.stop();
			} catch (LifecycleException e) {
				log.error("Catalina.stop: ", e);
			}
			return;
		}

		// Stop the existing server
		s = getServer();
		if (s.getPort() > 0) {
			Socket socket = null;
			OutputStream stream = null;
			try {
				socket = new Socket(s.getAddress(), s.getPort());
				stream = socket.getOutputStream();
				String shutdown = s.getShutdown();
				for (int i = 0; i < shutdown.length(); i++) {
					stream.write(shutdown.charAt(i));
				}
				stream.flush();
			} catch (ConnectException ce) {
				log.error(sm.getString("catalina.stopServer.connectException",
						s.getAddress(),
						String.valueOf(s.getPort())));
				log.error("Catalina.stop: ", ce);
				System.exit(1);
			} catch (IOException e) {
				log.error("Catalina.stop: ", e);
				System.exit(1);
			} finally {
				if (stream != null) {
					try {
						stream.close();
					} catch (IOException e) {
						// Ignore
					}
				}
				if (socket != null) {
					try {
						socket.close();
					} catch (IOException e) {
						// Ignore
					}
				}
			}
		} else {
			log.error(sm.getString("catalina.stopServer"));
			System.exit(1);
		}
	}

	/**
	 * Start a new server instance.
	 */
	public void load() {

		long t1 = System.nanoTime();

		initDirs();

		// Before digester - it may be needed

		initNaming();

		// Create and execute our Digester
		Digester digester = createStartDigester();

		InputSource inputSource = null;
		InputStream inputStream = null;
		File file = null;
		try {
			try {
				file = configFile();
				inputStream = new FileInputStream(file);
				inputSource = new InputSource(file.toURI().toURL().toString());
			} catch (Exception e) {
				if (log.isDebugEnabled()) {
					log.debug(sm.getString("catalina.configFail", file), e);
				}
			}
			if (inputStream == null) {
				try {
					inputStream = getClass().getClassLoader()
							.getResourceAsStream(getConfigFile());
					inputSource = new InputSource
							(getClass().getClassLoader()
									.getResource(getConfigFile()).toString());
				} catch (Exception e) {
					if (log.isDebugEnabled()) {
						log.debug(sm.getString("catalina.configFail",
								getConfigFile()), e);
					}
				}
			}

			// This should be included in catalina.jar
			// Alternative: don't bother with xml, just create it manually.
			if (inputStream == null) {
				try {
					inputStream = getClass().getClassLoader()
							.getResourceAsStream("server-embed.xml");
					inputSource = new InputSource
							(getClass().getClassLoader()
									.getResource("server-embed.xml").toString());
				} catch (Exception e) {
					if (log.isDebugEnabled()) {
						log.debug(sm.getString("catalina.configFail",
								"server-embed.xml"), e);
					}
				}
			}


			if (inputStream == null || inputSource == null) {
				if (file == null) {
					log.warn(sm.getString("catalina.configFail",
							getConfigFile() + "] or [server-embed.xml]"));
				} else {
					log.warn(sm.getString("catalina.configFail",
							file.getAbsolutePath()));
					if (file.exists() && !file.canRead()) {
						log.warn("Permissions incorrect, read permission is not allowed on the file.");
					}
				}
				return;
			}

			try {
				inputSource.setByteStream(inputStream);
				digester.push(this);
				digester.parse(inputSource);
			} catch (SAXParseException spe) {
				log.warn("Catalina.start using " + getConfigFile() + ": " +
						spe.getMessage());
				return;
			} catch (Exception e) {
				log.warn("Catalina.start using " + getConfigFile() + ": ", e);
				return;
			}
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (IOException e) {
					// Ignore
				}
			}
		}

		getServer().setCatalina(this);

		// Stream redirection
		initStreams();

		// Start the new server
		try {
			getServer().init();
		} catch (LifecycleException e) {
			if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
				throw new java.lang.Error(e);
			} else {
				log.error("Catalina.start", e);
			}

		}

		long t2 = System.nanoTime();
		if (log.isInfoEnabled()) {
			log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
		}

	}

	/*
	 * Load using arguments
	 */
	public void load(String args[]) {

		try {
			if (arguments(args)) {
				load();
			}
		} catch (Exception e) {
			e.printStackTrace(System.out);
		}
	}

	/**
	 * Start a new server instance.
	 */
	public void start() {

		if (getServer() == null) {
			load();
		}

		if (getServer() == null) {
			log.fatal("Cannot start server. Server instance is not configured.");
			return;
		}

		long t1 = System.nanoTime();

		// Start the new server
		try {
			getServer().start();
		} catch (LifecycleException e) {
			log.fatal(sm.getString("catalina.serverStartFail"), e);
			try {
				getServer().destroy();
			} catch (LifecycleException e1) {
				log.debug("destroy() failed for failed Server ", e1);
			}
			return;
		}

		long t2 = System.nanoTime();
		if (log.isInfoEnabled()) {
			log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
		}

		// Register shutdown hook
		if (useShutdownHook) {
			if (shutdownHook == null) {
				shutdownHook = new CatalinaShutdownHook();
			}
			Runtime.getRuntime().addShutdownHook(shutdownHook);

			// If JULI is being used, disable JULI's shutdown hook since
			// shutdown hooks run in parallel and log messages may be lost
			// if JULI's hook completes before the CatalinaShutdownHook()
			LogManager logManager = LogManager.getLogManager();
			if (logManager instanceof ClassLoaderLogManager) {
				((ClassLoaderLogManager) logManager).setUseShutdownHook(
						false);
			}
		}

		if (await) {
			await();
			stop();
		}
	}

	/**
	 * Stop an existing server instance.
	 */
	public void stop() {

		try {
			// Remove the ShutdownHook first so that server.stop()
			// doesn't get invoked twice
			if (useShutdownHook) {
				Runtime.getRuntime().removeShutdownHook(shutdownHook);

				// If JULI is being used, re-enable JULI's shutdown to ensure
				// log messages are not lost
				LogManager logManager = LogManager.getLogManager();
				if (logManager instanceof ClassLoaderLogManager) {
					((ClassLoaderLogManager) logManager).setUseShutdownHook(
							true);
				}
			}
		} catch (Throwable t) {
			ExceptionUtils.handleThrowable(t);
			// This will fail on JDK 1.2. Ignoring, as Tomcat can run
			// fine without the shutdown hook.
		}

		// Shut down the server
		try {
			Server s = getServer();
			LifecycleState state = s.getState();
			if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
					&& LifecycleState.DESTROYED.compareTo(state) >= 0) {
				// Nothing to do. stop() was already called
			} else {
				s.stop();
				s.destroy();
			}
		} catch (LifecycleException e) {
			log.error("Catalina.stop", e);
		}

	}

	/**
	 * Await and shutdown.
	 */
	public void await() {

		getServer().await();

	}

	/**
	 * Print usage information for this application.
	 */
	protected void usage() {

		System.out.println
				("usage: java org.apache.catalina.startup.Catalina"
						+ " [ -config {pathname} ]"
						+ " [ -nonaming ] "
						+ " { -help | start | stop }");

	}

	protected void initDirs() {

		String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP);
		if (catalinaHome == null) {
			// Backwards compatibility patch for J2EE RI 1.3
			String j2eeHome = System.getProperty("com.sun.enterprise.home");
			if (j2eeHome != null) {
				catalinaHome = System.getProperty("com.sun.enterprise.home");
			} else if (System.getProperty(Globals.CATALINA_BASE_PROP) != null) {
				catalinaHome = System.getProperty(Globals.CATALINA_BASE_PROP);
			}
		}
		// last resort - for minimal/embedded cases.
		if (catalinaHome == null) {
			catalinaHome = System.getProperty("user.dir");
		}
		if (catalinaHome != null) {
			File home = new File(catalinaHome);
			if (!home.isAbsolute()) {
				try {
					catalinaHome = home.getCanonicalPath();
				} catch (IOException e) {
					catalinaHome = home.getAbsolutePath();
				}
			}
			System.setProperty(Globals.CATALINA_HOME_PROP, catalinaHome);
		}

		if (System.getProperty(Globals.CATALINA_BASE_PROP) == null) {
			System.setProperty(Globals.CATALINA_BASE_PROP,
					catalinaHome);
		} else {
			String catalinaBase = System.getProperty(Globals.CATALINA_BASE_PROP);
			File base = new File(catalinaBase);
			if (!base.isAbsolute()) {
				try {
					catalinaBase = base.getCanonicalPath();
				} catch (IOException e) {
					catalinaBase = base.getAbsolutePath();
				}
			}
			System.setProperty(Globals.CATALINA_BASE_PROP, catalinaBase);
		}

		String temp = System.getProperty("java.io.tmpdir");
		if (temp == null || (!(new File(temp)).exists())
				|| (!(new File(temp)).isDirectory())) {
			log.error(sm.getString("embedded.notmp", temp));
		}

	}

	protected void initStreams() {
		// Replace System.out and System.err with a custom PrintStream
		System.setOut(new SystemLogHandler(System.out));
		System.setErr(new SystemLogHandler(System.err));
	}

	protected void initNaming() {
		// Setting additional variables
		if (!useNaming) {
			log.info("Catalina naming disabled");
			System.setProperty("catalina.useNaming", "false");
		} else {
			System.setProperty("catalina.useNaming", "true");
			String value = "org.apache.naming";
			String oldValue =
					System.getProperty(javax.naming.Context.URL_PKG_PREFIXES);
			if (oldValue != null) {
				value = value + ":" + oldValue;
			}
			System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value);
			if (log.isDebugEnabled()) {
				log.debug("Setting naming prefix=" + value);
			}
			value = System.getProperty
					(javax.naming.Context.INITIAL_CONTEXT_FACTORY);
			if (value == null) {
				System.setProperty
						(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
								"org.apache.naming.java.javaURLContextFactory");
			} else {
				log.debug("INITIAL_CONTEXT_FACTORY already set " + value);
			}
		}
	}


	// --------------------------------------- CatalinaShutdownHook Inner Class

	// XXX Should be moved to embedded !

	/**
	 * Set the security package access/protection.
	 */
	protected void setSecurityProtection() {
		SecurityConfig securityConfig = SecurityConfig.newInstance();
		securityConfig.setPackageDefinition();
		securityConfig.setPackageAccess();
	}

	/**
	 * Shutdown hook which will perform a clean shutdown of Catalina if needed.
	 */
	protected class CatalinaShutdownHook extends Thread {

		@Override
		public void run() {
			try {
				if (getServer() != null) {
					Catalina.this.stop();
				}
			} catch (Throwable ex) {
				ExceptionUtils.handleThrowable(ex);
				log.error(sm.getString("catalina.shutdownHookFail"), ex);
			} finally {
				// If JULI is used, shut JULI down *after* the server shuts down
				// so log messages aren't lost
				LogManager logManager = LogManager.getLogManager();
				if (logManager instanceof ClassLoaderLogManager) {
					((ClassLoaderLogManager) logManager).shutdown();
				}
			}
		}
	}

}


// ------------------------------------------------------------ Private Classes

/**
 * Rule that sets the parent class loader for the top object on the stack,
 * which must be a <code>Container</code>.
 */

final class SetParentClassLoaderRule extends Rule {

	ClassLoader parentClassLoader = null;

	public SetParentClassLoaderRule(ClassLoader parentClassLoader) {

		this.parentClassLoader = parentClassLoader;

	}

	@Override
	public void begin(String namespace, String name, Attributes attributes)
			throws Exception {

		if (digester.getLogger().isDebugEnabled()) {
			digester.getLogger().debug("Setting parent class loader");
		}

		Container top = (Container) digester.peek();
		top.setParentClassLoader(parentClassLoader);

	}


}
